Merge pull request #131 from umarsheikh/master

Public Transport Agent with specs

Andrew Cantino 11 年之前
父節點
當前提交
3fe4a31ffc

+ 118 - 0
app/models/agents/public_transport_agent.rb

@@ -0,0 +1,118 @@
1
+require 'date'
2
+require 'cgi'
3
+module Agents
4
+  class PublicTransportAgent < Agent
5
+    cannot_receive_events!
6
+    description <<-MD
7
+      Specify the following user settings:
8
+
9
+      * stops (array)
10
+      * agency (string)
11
+      * alert_window_in_minutes (integer)
12
+
13
+      This Agent generates Events based on NextBus GPS transit predictions.  First, select an agency by visiting [http://www.nextbus.com/predictor/agencySelector.jsp](http://www.nextbus.com/predictor/agencySelector.jsp) and finding your transit system.  Once you find it, copy the part of the URL after `?a=`.  For example, for the San Francisco MUNI system, you would end up on [http://www.nextbus.com/predictor/stopSelector.jsp?a=**sf-muni**](http://www.nextbus.com/predictor/stopSelector.jsp?a=sf-muni) and copy "sf-muni".  Put that into this Agent's agency setting.
14
+
15
+      Next, find the stop tags that you care about.  To find the tags for the sf-muni system, for the N route, visit this URL:
16
+      [http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=**N**](http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=N)
17
+
18
+      The tags are listed as tag="1234". Copy that number and add the route before it, separated by a pipe '&#124;' symbol.  Once you have one or more tags from that page, add them to this Agent's stop list.  E.g,
19
+
20
+          agency: "sf-muni"
21
+          stops: ["N|5221", "N|5215"]
22
+
23
+      This Agent will generate predictions by requesting a URL similar to the following:
24
+
25
+      [http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N&#124;5221&stops=N&#124;5215](http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N&#124;5221&stops=N&#124;5215)
26
+
27
+      Finally, set the arrival window that you're interested in.  E.g., 5 minutes.  Events will be created by the agent anytime a new train or bus comes into that time window.
28
+
29
+          alert_window_in_minutes: 5
30
+
31
+      This memory should get cleaned up when timestamp is older than an hour (or something) so that it doesn't fill up all of the Agent's memory.
32
+    MD
33
+
34
+
35
+    default_schedule "every_2m"
36
+
37
+    event_description <<-MD
38
+    Events look like this:
39
+      { "routeTitle":"N-Judah",
40
+        "stopTag":"5215",
41
+        "prediction":
42
+           {"epochTime":"1389622846689",
43
+            "seconds":"3454","minutes":"57","isDeparture":"false",
44
+            "affectedByLayover":"true","dirTag":"N__OB4KJU","vehicle":"1489",
45
+            "block":"9709","tripTag":"5840086"
46
+            }
47
+      }
48
+    MD
49
+
50
+    def check_url
51
+      stop_query = URI.encode(options["stops"].collect{|a| "&stops=#{a}"}.join)
52
+      "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=#{options["agency"]}#{stop_query}"
53
+    end
54
+
55
+    def stops
56
+      options["stops"].collect{|a| a.split("|").last}
57
+    end
58
+    def check
59
+      hydra = Typhoeus::Hydra.new
60
+      request = Typhoeus::Request.new(check_url, :followlocation => true)
61
+      request.on_success do |response|
62
+        page = Nokogiri::XML response.body
63
+        predictions = page.css("//prediction")
64
+        predictions.each do |pr|
65
+          parent = pr.parent.parent
66
+          vals = {"routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"]}
67
+          if pr["minutes"] && pr["minutes"].to_i < options["alert_window_in_minutes"].to_i
68
+            vals = vals.merge Hash.from_xml(pr.to_xml)
69
+            if not_already_in_memory?(vals)
70
+              create_event(:payload => vals)
71
+              log "creating event..."
72
+              update_memory(vals)
73
+            else
74
+              log "not creating event since already in memory"
75
+            end
76
+          end
77
+        end
78
+      end
79
+      hydra.queue request
80
+      hydra.run
81
+    end
82
+    def update_memory(vals)
83
+      add_to_memory(vals)
84
+      cleanup_old_memory
85
+    end
86
+    def cleanup_old_memory
87
+      self.memory["existing_routes"] ||= []
88
+      self.memory["existing_routes"].reject!{|h| h["currentTime"].to_time <= (Time.now - 2.hours)}
89
+    end
90
+    def add_to_memory(vals)
91
+      self.memory["existing_routes"] ||= []
92
+      self.memory["existing_routes"] << {"stopTag" => vals["stopTag"], "tripTag" => vals["prediction"]["tripTag"], "epochTime" => vals["prediction"]["epochTime"], "currentTime" => Time.now}
93
+    end
94
+    def not_already_in_memory?(vals)
95
+      m = self.memory["existing_routes"] || []
96
+      m.select{|h| h['stopTag'] == vals["stopTag"] &&
97
+                h['tripTag'] == vals["prediction"]["tripTag"] &&
98
+                h['epochTime'] == vals["prediction"]["epochTime"]
99
+              }.count == 0
100
+    end
101
+    def default_options
102
+      {
103
+        agency: "sf-muni",
104
+        stops: ["N|5221", "N|5215"],
105
+        alert_window_in_minutes: 5
106
+      }
107
+    end
108
+
109
+    def validate_options
110
+      errors.add(:base, 'agency is required') unless options['agency'].present?
111
+      errors.add(:base, 'alert_window_in_minutes is required') unless options['alert_window_in_minutes'].present?
112
+      errors.add(:base, 'stops are required') unless options['stops'].present?
113
+    end
114
+    def working?
115
+      event_created_within?(2) && !recent_error_logs?
116
+    end
117
+  end
118
+end

+ 35 - 0
spec/data_fixtures/public_transport_agent.xml

@@ -0,0 +1,35 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<body copyright="All data copyright San Francisco Muni 2014.">
3
+<predictions agencyTitle="San Francisco Muni" routeTitle="N-Judah" routeTag="N" stopTitle="Judah St &amp; La Playa St" stopTag="5221">
4
+  <direction title="Outbound to Ocean Beach">
5
+  <prediction epochTime="1389707083293" seconds="1668" minutes="27" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1443" block="9705" tripTag="5840326"/>
6
+  <prediction epochTime="1389708835605" seconds="3420" minutes="57" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1518" block="9708" tripTag="5840327"/>
7
+  <prediction epochTime="1389709795605" seconds="4380" minutes="73" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1404" block="9710" tripTag="5840328"/>
8
+  </direction>
9
+  <direction title="Outbound to Ocean Beach via Downtown">
10
+  <prediction epochTime="1389706393991" seconds="978" minutes="16" isDeparture="false" dirTag="N__OB4KJU" vehicle="1543" vehiclesInConsist="2" block="9703" tripTag="5840324"/>
11
+  <prediction epochTime="1389706512784" seconds="1097" minutes="18" isDeparture="false" dirTag="N__OB4KJU" vehicle="1476" vehiclesInConsist="2" block="9704" tripTag="5840083"/>
12
+  <prediction epochTime="1389707746994" seconds="2331" minutes="38" isDeparture="false" dirTag="N__OB4KJU" vehicle="1507" block="9706" tripTag="5840084"/>
13
+  <prediction epochTime="1389708458668" seconds="3043" minutes="50" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1489" block="9707" tripTag="5840085"/>
14
+  <prediction epochTime="1389709358668" seconds="3943" minutes="65" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1463" block="9709" tripTag="5840086"/>
15
+  </direction>
16
+<message text="No Elevator at
17
+Van Ness Station"/>
18
+</predictions>
19
+<predictions agencyTitle="San Francisco Muni" routeTitle="N-Judah" routeTag="N" stopTitle="Judah St &amp; 46th Ave" stopTag="5215">
20
+  <direction title="Outbound to Ocean Beach">
21
+  <prediction epochTime="1389706981164" seconds="1566" minutes="26" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1443" block="9705" tripTag="5840326"/>
22
+  <prediction epochTime="1389708733476" seconds="3318" minutes="55" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1518" block="9708" tripTag="5840327"/>
23
+  <prediction epochTime="1389709693476" seconds="4278" minutes="71" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1404" block="9710" tripTag="5840328"/>
24
+  </direction>
25
+  <direction title="Outbound to Ocean Beach via Downtown">
26
+  <prediction epochTime="1389706282012" seconds="866" minutes="14" isDeparture="false" dirTag="N__OB4KJU" vehicle="1543" vehiclesInConsist="2" block="9703" tripTag="5840324"/>
27
+  <prediction epochTime="1389706400805" seconds="985" minutes="16" isDeparture="false" dirTag="N__OB4KJU" vehicle="1476" vehiclesInConsist="2" block="9704" tripTag="5840083"/>
28
+  <prediction epochTime="1389707635015" seconds="2219" minutes="36" isDeparture="false" dirTag="N__OB4KJU" vehicle="1507" block="9706" tripTag="5840084"/>
29
+  <prediction epochTime="1389708346689" seconds="2931" minutes="48" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1489" block="9707" tripTag="5840085"/>
30
+  <prediction epochTime="1389709246689" seconds="3831" minutes="63" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1463" block="9709" tripTag="5840086"/>
31
+  </direction>
32
+<message text="No Elevator at
33
+Van Ness Station"/>
34
+</predictions>
35
+</body>

+ 70 - 0
spec/models/agents/public_transport_agent_spec.rb

@@ -0,0 +1,70 @@
1
+require 'spec_helper'
2
+describe Agents::PublicTransportAgent do
3
+  before do
4
+    valid_params = {
5
+      "name" => "sf muni agent",
6
+      "options" => {
7
+        "alert_window_in_minutes" => "20",
8
+        "stops" => ['N|5221', 'N|5215'],
9
+        "agency" => "sf-muni"
10
+      }
11
+    }
12
+    @agent = Agents::PublicTransportAgent.new(valid_params)
13
+    @agent.user = users(:bob)
14
+    @agent.save!
15
+  end
16
+
17
+  describe "#check" do
18
+    before do
19
+      stub_request(:get, "http://webservices.nextbus.com/service/publicXMLFeed?a=sf-muni&command=predictionsForMultiStops&stops=N%7C5215").
20
+         with(:headers => {'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}).
21
+         to_return(:status => 200, :body => File.read(Rails.root.join("spec/data_fixtures/public_transport_agent.xml")), :headers => {})
22
+      stub(Time).now {"2014-01-14 20:21:30 +0500".to_time}
23
+    end
24
+
25
+    it "should create 4 events" do
26
+      lambda { @agent.check }.should change {@agent.events.count}.by(4)
27
+    end
28
+
29
+    it "should add 4 items to memory" do
30
+      @agent.memory.should == {}
31
+      @agent.check
32
+      @agent.memory.should == {"existing_routes" => [
33
+          {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>"2014-01-14 20:21:30 +0500"},
34
+          {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>"2014-01-14 20:21:30 +0500"},
35
+          {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>"2014-01-14 20:21:30 +0500"},
36
+          {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>"2014-01-14 20:21:30 +0500"}
37
+        ]
38
+      }
39
+    end
40
+
41
+    it "should not create events twice" do
42
+      lambda { @agent.check }.should change {@agent.events.count}.by(4)
43
+      lambda { @agent.check }.should_not change {@agent.events.count}
44
+    end
45
+
46
+    it "should reset memory after 2 hours" do
47
+      lambda { @agent.check }.should change {@agent.events.count}.by(4)
48
+      stub(Time).now {"2014-01-14 20:21:30 +0500".to_time + 3.hours}
49
+      @agent.cleanup_old_memory
50
+      lambda { @agent.check }.should change {@agent.events.count}.by(4)
51
+    end
52
+  end
53
+
54
+  describe "validation" do
55
+    it "should validate presence of stops" do
56
+      @agent.options['stops'] = nil
57
+      @agent.should_not be_valid
58
+    end
59
+
60
+    it "should validate presence of agency" do
61
+      @agent.options['agency'] = ""
62
+      @agent.should_not be_valid
63
+    end
64
+
65
+    it "should validate presence of alert_window_in_minutes" do
66
+      @agent.options['alert_window_in_minutes'] = ""
67
+      @agent.should_not be_valid
68
+    end
69
+  end
70
+end